'''
Created on Aug 2, 2010

@author: vijayp
'''
from ting3.common.t3expvar import *

import tornado.web
from tornado import httpserver
import time
import logging

import os
import thread
import inspect
import sys
import functools

from tornado.ioloop import IOLoop, PeriodicCallback

class BadQuantaError(Exception):
  pass

class BaseWatchedVariable(object):
  ''' base class for a watched variable. '''
  def AsList(self):
    assert 0
    
  def AsString(self):
    return '\n'.join([' '.join(x) for x in self.AsList()])
  
class WatchedVariable(BaseWatchedVariable):
  ''' This class watches a variable and generates a [("variable name", 
                                                      "variable value")]. 
  It will automatically register and export this variable through a ExpDH 
  singleton. To export your variable. (for now it has to be a class variable, 
  unless you specify a name)
  
  class A:
    def __init__(self):
      self.myval = 1
      WatchedVariable(self, 'myval')
  
  a = A()
  
  This will automatically export a variable called "A.a" which tracks the value
  appropriately. Note that if you have multiple instances of your class, only 
  one will be exported using the default name. If you want to have multiple of 
  them exported, you must set container_name.
  
  AsList() will return [('A.a', '1')]
  
  If you want, you can also set getval_fcn, which can be any function that 
  returns a numeric value.   
  '''
  def __init__(self, obj, thing_name, container_name=None, getval_fcn=None):
    assert type(thing_name) == type('')
    if container_name == None:
      container_name = obj.__class__.__name__
    self._name = container_name + '.' + thing_name
    self._obj, self._thing_name = (obj, thing_name)
    self._getval = functools.partial(getattr,self._obj, self._thing_name) if getval_fcn is None else getval_fcn
    ExpDH.instance().RegisterWatcher(self)

  def refresh_period(self): 
    return None     
  
  def AsList(self):
    v = self._getval()
    if type(v) in ExpDH.instance().OK_TYPES:
      return[(self._name, str(v))]
    return []
  
class RateTracker(WatchedVariable):
  ''' This class watches a variable, and but also tracks rate information.
  For each variable, it generates one line, as above. However it also tracks
  the change in the value in different quanta. These are reported as follows:

    AsList() will return [('A.a', '1'), 
                          ('A.a-1', '0') #activity in the last second
                          ...
                          ]
                           
  You probably shouldn't use this class, but instead, use one of the classes 
  below, or inherit from this.  It's a bit inefficient for large quanta, so be 
  judicious in its use.
  
  Also, all the quanta must be multiples of the smallest quanta. And this
  requires a running ioloop to work properly. 
  ''' 
  DEFAULT_QUANTA = [1, 10, 100]
  
  def __init__(self, obj, thing_name, ioloop=None, 
               container_name=None, getval_fcn=None, quanta=None):
    
    self._quanta = quanta if quanta is not None else self.DEFAULT_QUANTA[:]
    self._quanta.sort()
    self._smallest_quantum = self._quanta[0]
    self._ioloop = ioloop if ioloop is not None else IOLoop.instance()
    for q in self._quanta:
      if q % self._smallest_quantum != 0:
        raise BadQuantaError, q

    super(RateTracker, self).__init__(obj, thing_name,
                                         container_name=container_name, 
                                         getval_fcn=getval_fcn)

    self._dat = [[self._getval()]]
    for _ in range(len(self._quanta) -1):
      self._dat.append([])
    self._cnt = [0] * len(self._quanta)
    assert len(self._dat) == len(self._quanta)

    
  def refresh_period(self):
    return self._smallest_quantum
  
  def __FillIfFull(self, offset, value=None):
    #TODO: be more efficient by tracking deltas only, and using varint.
    if offset == len(self._dat): 
      return  
    
    if value is None:
      value = self._getval()
      
    max_cnt = self._quanta[offset] / self._smallest_quantum
    self._dat[offset].append(value)
    if len(self._dat[offset]) >= max_cnt:
      x = self._dat[offset].pop(0)
      self.__FillIfFull(offset+1, x)
      
       
  def MinQuantumExpired(self):
    logging.debug('quantum expired for %s (%s)', self._name, self._getval())
    self.__FillIfFull(0)
    
  def get(self, quantum):
    assert quantum in self._quanta
    
    idx = self._quanta.index(quantum)
    if len(self._dat[idx]) == 0:
      if idx == 0:
        assert 0
      else:
        return self.get(self._quanta[idx-1])
      
    return  self._getval() - self._dat[idx][0] 
  
  def AsList(self):
    rv = [(self._name, str(self._getval()))]
    for q in self._quanta:
      rv += [( ('%s-%0.5d' % (self._name, q)), 
               str(self.get(q)))]

    return rv
      
###############################

class ExpRate_1M10M1H(RateTracker):
  DEFAULT_QUANTA = [60,600,3600]

class ExpRate_1M10M1H1D(RateTracker):
  DEFAULT_QUANTA = [60, 600,3600,3600*24]

###############################
class ExpDH(object):
  ''' This class is meant to be a singleton object. It monitors and tracks 
  variables in other classes for the purposes of easy monitoring.  
  
  This class relies on:
   - ioloop
  And for it to be useful, you probably want to run a 
   - Ting3HttpServer
   
  You can register your special variable by (preferably), creating some child of 
  BaseWatchedVariable, above.  
  
  We try to automatically guess the name we should use for the variable, but 
  it may not be to your liking, so you might want to specify that.
  
  The AutoAddClass method automatically exports anything in your class that has 
  a name ending in "_exp". This is very expensive (lots of introspection!).
  
  If you want to get access to the singleton, use ExpDH.instance().
  
  '''
  OK_TYPES = [type(None),type(1), type(1.0), type('asdf'), 
                  type([]), type((1,)), type(True)]

  @classmethod
  def instance(cls):
    if not hasattr(cls, "_instance"):
        cls._instance = cls()
    return cls._instance

  def set(self, key, value):
    self._variable_map[key] = value
  
  def setTitle(self, title):
    self._title = title
    
  def inc(self, key):
    # TODO(vijayp): collect rate information
    self._variable_map[key] = 1 + self._variable_map.setdefault(key,0)
    
  def __init__(self, ioloop=None):
    self._variable_map = {}
    self._things = {}
    self._title = 'Unknown server'
    self._periodic_logger = None
    
    # mapping from min quantum to (callback, list of variables)
    self._watched_vars = {}
    self._ioloop = ioloop if ioloop is not None else IOLoop.instance()
    
  def AutoAddClass(self, thing, name=None):
    ''' Monitor a class, and automatically export anything that ends in "_exp" 
    at query time. This can be VERY expensive, so use sparingly.
    '''
    
    if name is None:
      name = thing.__class__.__name__
      
    self._things[name] =  thing
  
  #DEPRECATED!
  def WatchVariable(self, *args, **kwargs):
    '''DON'T USE THIS.'''
    return WatchedVariable(*args, **kwargs)
  
  def _QuantumCallback(self, quantum_id):
    logging.debug('work up for callback work at %s' % time.time() )
    for v in self._watched_vars[quantum_id][1]:
      v.MinQuantumExpired()
      
  def Log(self, level):
    logging.info('logging stuff')
    logging.log(level, self.AsText())

  def PeriodicLogger(self, interval_s):
    logging.info('enqueuing periodic wakeup for time period %d', interval_s)
    if self._periodic_logger is not None:
      self._periodic_logger.stop()
    self._periodic_logger = PeriodicCallback(
                                             functools.partial(self.Log, 
                                                               logging.INFO),
                                             interval_s * 1000,
                                             io_loop=self._ioloop)
    self._periodic_logger.start()

  def RegisterWatcher(self, var):
    ''' this should only be used by a BaseWatchedVariable instance'''
    
    #TODO: aggregate min quanta that are multiples of one another
    period = var.refresh_period()
    if not self._watched_vars.has_key(period):
      if period is not None:
        cb = PeriodicCallback(functools.partial(self._QuantumCallback, period), 
                              period*1000, 
                              io_loop = self._ioloop)
        cb.start()
      else:
        cb = None 
      self._watched_vars[period] = (cb, [var])
    else:
      self._watched_vars[period][1].append(var)
    
  def _GetMonitoredList(self):
    tmp_map = {}
    for name, thing in self._things.items():
      for field in thing.__dict__:
        if type(getattr(thing, field)) in self.OK_TYPES and field.endswith('_exp'):
          tmp_map[name + '.' + field] = str(getattr(thing, field))
    
    return tmp_map.items()
  
  def _GetVariableList(self):
    self._variable_map['time'] = time.time()
    self._variable_map['pid'] = str(os.getpid())
    self._variable_map['title'] = self._title

    wlst = []
    for _, vars in self._watched_vars.values():
      for w in vars:
        wlst += w.AsList()

    return sorted([(str(x), str(y)) for x,y in self._variable_map.items()]
                   + self._GetMonitoredList() + wlst)
  
  def AsText(self):
    return '\n'.join([' '.join(map(str,x)) for x in self._GetVariableList()])
  
  def AsHTML(self):
    ''' TODO: fix this junk up'''
    
    H = lambda x:x.encode('ascii', 'xmlcharrefreplace')
    
    #TODO: remove string copying
    outtext = '''<h1> Data for %s </h2> <table border=2>''' % H(self._title)
    
    for k,v in self._GetVariableList():
      outtext += '<tr><td width=20%%>%s</td><td>%s</td></tr>' % (H(k), H(v)) 
    outtext += '</table>'
    return outtext
